在介绍垃圾回收算法之前,还需要对对象分配和释放做一些讲解,需要清楚的知道堆中的哪些会被回收掉。此外,本节会简要介绍对象是如何分配,以及如何回收的。
一般来说,为对象分配内存时,并不会直接在堆上划分内存,而是先在线程局部缓冲(thread local buffer)或其他类似的结构中找地方放置对象,然后随着应用程序的运行,这些对象可能会被提升到堆中,最终还是要在堆中为新对象查找合适的空间。
为了能够在堆中给新创建的对象找一个合适的位置,内存管理系统必须知道堆中有哪些地方是空闲的,即还没有存货对象占用。内存管理系统使用 空闲列表来管理内存中可用的空闲区域,并按照某个维度的优先级进行排序。
在空闲列表中搜索足够存储新对象的空闲块时,可以选择大小最适合的空闲块,也可以选择第一个放得下的空闲块。这其中会用到几种不同的算法去实现,各有优劣,后文会详细讨论。
在实际应用中,仅仅跟踪空闲空间是不够的,还有一些其他问题要处理, 碎片化就是内存管理器要面对的一大难题。当死对象(译者注,就是那些再也无法使用的对象)被垃圾回收器清除后,就会在堆上留下一个个的 空洞。
碎片化问题大大制约了垃圾回收的伸缩性,严重的时候,即便堆中还有大量空闲空间可用,但却无法为新对象找到合适的存储位置。这时,为了能够清理出足够大的空间来放置新对象,运行时系统一般会频繁GC,但却仍旧无法为新对象腾出足够大的连续空间,于是运行时系统陷入了死循环。
下面的图展示了堆中几个对象的分布:
上面图中所有的对象都是存活的,对象 A占据了2个存储单元,其他对象各占一个。当垃圾回收开始的时候,有两个对象是 可达的(reachable),它们各自形成了独立的对象关系图,分别是 ABCD和EFGH。
此时,如果将指向对象 E的引用赋值为null
,则 E及其所指向的对象 FGH都会被当做垃圾回收掉。然后,堆上的对象分布如下所示:
经过垃圾回收后,堆上总共有4个空闲的存储单元,但即便如此,还是无法放下一个体积大于1的对象,这时就会抛出OutOfMemoryError
错误,这就是内存碎片的问题。
因此,内存管理系统为了能够腾出足够大的连续内存空间,就会采取一些特殊措施,这个过程称之为 整理(compaction)。在垃圾回收周期中, 整理是一个独立的阶段,在该阶段中会将经过垃圾回收后的存活对象移到一起。
下图是经过整理之后的堆:
现在,堆上已经有了一个大小为4的连续存储空间,可以存放以往因内存不足而无法存放的对象了。
但遗憾的是,一般情况下, 整理是一个STW式的操作,并发执行困难较大,本章会在后面介绍一些可以提升效率的办法(在第5章和第13章将做一些扩展介绍)。
在执行整理操作时,会遍历对象的引用关系图,并假设互相引用的对象很有可能会被依次访问到,所以垃圾回收器会尽量将有引用关系的对象紧挨着放在一起,这样做是为了可以更好的利用缓存,同时,如果对象的生命周期差不多的话,就可以在垃圾回收后得到更大的连续空间。
各种垃圾回收算法可以不同程度上抑制内存碎片化的进程(例如使用分代垃圾回收),或者实现自动整理(例如暂停并复制),这些内容将在后文详细介绍。